﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using Medianamik.Core.Action;
using Medianamik.Core.Caching;
using Medianamik.Core.Interfaces;
using Medianamik.Core.Lock;
using Medianamik.Core.Querying;
using Medianamik.Core.Security;
using Medianamik.Core.Sugar;

namespace Medianamik.Core
{
    public static class NodeManager
    {
        private static readonly object Locker = new object();

        public static INodeCacheProvider CacheProvider;

        public static Node GetNode(Guid nodeId)
        {
            return NewQuery().Where(n => n.ID, Comparison.Equal, nodeId)
                .Execute().FirstOrDefault();
        }

        public static Node GetNodeEvenIfDeleted(Guid nodeId)
        {
            return NewQuery(true).Where(n => n.ID, Comparison.Equal, nodeId)
                .Execute().FirstOrDefault();
        }

        public static T GetNode<T>(Guid nodeId, Guid typeId)
            where T : INodeWrapper, new()
        {
            var node = NewQuery()
                .Where(n => n.ID, Comparison.Equal, nodeId)
                .Where(n => n.NodeTypeID, Comparison.Equal, typeId)
                .Execute().FirstOrDefault();

            return node == null ? default(T) : new T { InnerNode = node };
        }

        public static T GetNode<T>(Guid nodeId)
            where T : INodeWrapper, new()
        {
            var node = GetNode(nodeId);

            return node == null ? default(T) : new T { InnerNode = node };
        }

        public static Collection<T> GetNodes<T>(IEnumerable<Guid> pIDs)
            where T : INodeWrapper, new()
        {
            return new Collection<T>(GetNodes(pIDs)
                                         .Select(n => new T { InnerNode = n })
                                         .ToList());
        }

        public static Collection<Node> GetNodes(IEnumerable<Guid> nodeIDs)
        {
            var nodes = new List<Node>();
            var totalCount = nodeIDs.Count();
            var pageIndex = 0;
            const int pageSize = 100;
            do
            {
                nodes.AddRange(GetNodes(nodeIDs, pageIndex, pageSize));
                pageIndex++;
            } while (pageIndex * pageSize < totalCount);

            return new Collection<Node>(nodes);
        }

        private static IEnumerable<Node> GetNodes(IEnumerable<Guid> pIds, int pPageIndex, int pPageSize)
        {
            var nodeIds = pIds.Skip(pPageIndex * pPageSize)
                .Take(pPageSize)
                .ToArray();

            if (nodeIds.Length > 0)
            {
                return NewQuery()
                    .Where(n => n.ID, Comparison.In, nodeIds).Execute();
            }
            return Enumerable.Empty<Node>();
        }

        public static Collection<Node> GetRootNodes()
        {
            var query = NewQuery()
                .Where(n => n.ParentID, Comparison.Equal, default(Guid?));

            return new Collection<Node>(query.Execute().ToList());
        }

        public static Collection<Node> GetRootNodes(Guid pNodeTypeId)
        {
            var type = NodeTypeManager.GetNodeType(pNodeTypeId);
            if (type == null)
            {
                throw new ArgumentException("NodeType does not exists:" + pNodeTypeId);
            }
            var types = NodeTypeManager.GetAllChildTypes(pNodeTypeId).Concat(new[] { type });

            var query = NewQuery()
                .Where(n => n.ParentID, Comparison.Equal, default(Guid?))
                .Where(n => n.NodeTypeID, Comparison.In, types.Select(t => t.ID).ToArray());

            return new Collection<Node>(query.Execute().ToList());
        }

        public static Collection<T> GetNodes<T>(Param pParam, Guid typeId)
            where T : INodeWrapper, new()
        {
            return new Collection<T>(GetNodes(pParam, typeId)
                                         .Select(n => new T { InnerNode = n })
                                         .ToList());
        }

        public static Collection<Node> GetNodesEvenIfDeleted(Param param, Guid typeId)
        {
            return GetNodes(param, typeId, true);
        }

        public static Collection<Node> GetNodesEvenIfDeletedWithTypeSpecified(Param param,
            string typeName)
        {
            if (param.Values.Count > 1)
                throw new NotImplementedException(
                    "GetNodesEvenIfDeletedWithTypeSpecified with parameters is not implemented for more than one value by parameter.");

            var type = NodeTypeManager.GetNodeType(typeName);
            var query = MakeGetNodeByTypeQuery(type.ID, true, true).Where(param.PropertyID,
                Comparison.Equal, param.Values.First().ToString());
            return new Collection<Node>(query.Execute().ToList());
        }

        public static Collection<Node> GetNodes(Param pParam, Guid typeId)
        {
            return GetNodes(pParam, typeId, false);
        }

        private static Collection<Node> GetNodes(Param pParam, Guid typeId, bool withDeleted)
        {
            if (pParam.Values.Count > 1)
                throw new NotImplementedException(
                    "GetNodes with parameters is not implemented for more than one value by parameter.");

            var query = MakeGetNodeByTypeQuery(typeId, true, withDeleted)
                .Where(pParam.PropertyID, Comparison.Equal, pParam.Values.First().ToString());

            return new Collection<Node>(query.Execute().ToList());
        }

        public static List<Node> GetNodesByType(Guid typeId)
        {
            return GetNodesByType(typeId, false);
        }

        public static List<Node> GetNodesByType(Guid typeId, bool blockInheritance)
        {
            return GetNodesByType(typeId, blockInheritance, -1, -1);
        }

        public static List<Node> GetNodesByType(Guid typeId, bool blockInheritance,
            int pageIndex, int count)
        {
            var query = MakeGetNodeByTypeQuery(typeId, blockInheritance);
            if (pageIndex >= 0)
            {
                query.Skip(pageIndex * count);
            }
            if (count > 0)
            {
                query.Take(count);
            }
            return query.Execute().ToList();
        }

        public static int CountNodesByType(Guid typeId)
        {
            return CountNodesByType(typeId, false);
        }

        public static int CountNodesByType(Guid typeId, bool blockInheritance)
        {
            var query = MakeGetNodeByTypeQuery(typeId, blockInheritance);
            return query.Count();
        }

        public static Query MakeGetNodeByTypeQuery(Guid typeId, bool blockInheritance)
        {
            return MakeGetNodeByTypeQuery(typeId, blockInheritance, false);
        }

        public static Query MakeGetNodeByTypeQuery(Guid typeId, bool blockInheritance,
            bool withDeleted)
        {
            var query = NewQuery(withDeleted);

            if (blockInheritance)
            {
                query.Where(n => n.NodeTypeID, Comparison.Equal, typeId);
            }
            else
            {
                var type = NodeTypeManager.GetNodeType(typeId);

                if (type == null)
                {
                    throw new ArgumentException("NodeType does not exists:" + typeId);
                }

                var types = NodeTypeManager.GetAllChildTypes(typeId).Concat(new[] { type });
                query.Where(n => n.NodeTypeID, Comparison.In, types.Select(t => t.ID).ToArray());
            }

            return query;
        }

        public static int CountChildNodesByType(INode parentNode, Guid baseNodeTypeId,
            bool typeInheritance)
        {
            return CountChildNodesByType(parentNode, NodeTypeManager.GetNodeType(baseNodeTypeId), typeInheritance);
        }

        public static int CountChildNodesByType(INode parentNode, NodeType baseNodeType,
            bool typeInheritance)
        {
            var baseNodeTypePath = baseNodeType == null
                                       ? string.Empty
                                       : baseNodeType.Path;

            var parentNodePath = parentNode == null
                                     ? string.Empty
                                     : parentNode.Path;

            var query = NewQuery()
                .Where(n => n.Path, Comparison.Like, parentNodePath
                                                     + DataUtil.GetSQLInclusivityOperator(false))
                .WhereType(t => t.Path, Comparison.Like, baseNodeTypePath
                                                         + DataUtil.GetSQLInclusivityOperator(typeInheritance));

            var count = query.Count();

            return count;
        }

        public static List<T> GetNodesByType<T>(bool blockInheritance)
            where T : ISystemNode, INodeWrapper, new()
        {
            return GetNodesByType(new T().SystemNodeTypeID)
                .Select(n => new T { InnerNode = n })
                .ToList();
        }

        public static Collection<Node> GetModifiedNodes(CultureInfo culture, Guid? baseNodeTypeId,
            Guid? pBaseNodeId)
        {
            var node = pBaseNodeId.HasValue
                           ? GetNode(pBaseNodeId.Value)
                           : default(Node);
            var type = baseNodeTypeId.HasValue
                           ? NodeTypeManager.GetNodeType(baseNodeTypeId.Value)
                           : default(NodeType);

            return GetModifiedNodes(culture, type, node);
        }

        public static Collection<Node> GetModifiedNodes(CultureInfo culture, NodeType baseNodeType,
            Node pBaseNode)
        {
            var baseNodeTypePath = baseNodeType == null
                                       ? string.Empty
                                       : baseNodeType.Path;
            var baseNodePath = pBaseNode == null
                                   ? string.Empty
                                   : pBaseNode.Path;
            var query = NewQuery().Where(n => n.Path, Comparison.Like,
                                         baseNodePath + DataUtil.GetSQLInclusivityOperator(true))
                .WhereType(t => t.Path, Comparison.Like, baseNodeTypePath + DataUtil.GetSQLInclusivityOperator(true))
                .WhereType(t => t.IsVersioned, Comparison.Equal, true)
                .HavingModification(culture);

            return new Collection<Node>(query.Execute().Where(n => !n.NodeType.IsSystem).ToList());

            //var nodes = DALProviderManager.DALProvider.NodeDALProvider.GetModifiedNodes(culture, baseNodeTypePath,baseNodePath);
            //return new Collection<Node>(nodes.Where(n => n.NodeType.IsSystem == false).ToList());
        }

        public static void PublishNode(Guid nodeId, CultureInfo culture)
        {
            PublishNodes(new[] { nodeId }, culture);
        }

        public static void PublishNodes(IEnumerable<Guid> nodeIDs, CultureInfo culture)
        {
            PublishNodes(GetNodes(nodeIDs.ToArray()), culture);
        }

        public static void PublishNodes(IEnumerable<Node> nodes, CultureInfo culture)
        {
            var instances = nodes.Where(n => n.IsVersioned).SelectMany(n => n.Instances)
                .Where(ni => ni.Culture.Name.Equals(culture.Name, StringComparison.InvariantCultureIgnoreCase));

            foreach (var instance in instances)
            {
                instance.ToBePublished = true;
            }

            SaveNodes(nodes);
        }

        //Supprime les modifications sur un Noeud en édition
        public static Node RevertNode(Guid nodeId, CultureInfo culture)
        {
            var node = GetNode(nodeId);
            if (culture.Name != "" && node.Instances.Exists(culture) &&
                node.Instances[culture].Revisions.Count() == 0)
            {
                if (node.Instances.Count == 1)
                {
                    DeleteNode(node);
                    return null;
                }

                node.RemoveNodeInstance(culture.Name);
                SaveNode(node);
            }
            else
            {
                DALProviderManager.DALProvider.NodeDALProvider.DeleteNodePropertyValues(nodeId, culture.Name, null);
                DALProviderManager.DALProvider.NodeDALProvider.DeleteNodePropertyValues(nodeId,
                                                                                        CultureInfo.InvariantCulture.
                                                                                            Name, null);

                //On reload le noeud en bypassant la cache
                node = new Query(new NullCacheProvider(), new QueryDefinition())
                    .Where(n => n.ID, Comparison.Equal, nodeId)
                    .Execute().FirstOrDefault();
                var instance = node.Instances.FirstOrDefault(ni => ni.Culture.Name.EqualsIgnoreCase(culture.Name));

                if (instance != null)
                {
                    instance.HasModifiedProperties = false;
                    instance.IsDirty = true;
                    if (node.HasName)
                    {
                        instance.Name = instance[node.NamePropertyId].GetValue<string>();
                    }
                    SaveNode(node);
                    ActionManager.Instance.OnAfterRevertNodeInstance(new ActionEventArgs<NodeInstance>(instance));
                }
            }

            return (node);
        }

        //Revert un noeud complet plutôt qu'une instance 
        public static Node RevertNode(Guid nodeId)
        {
            var node = GetNode(nodeId);

            if (node.DefaultInstance.Revisions.Count() == 0)
            {
                DeleteBranch(nodeId);
                return null;
            }

            foreach (var instance in node.Instances.Where(i => i.Culture.Name != ""))
            {
                DALProviderManager.DALProvider.NodeDALProvider.DeleteNodePropertyValues(nodeId, instance.Culture.Name,
                                                                                        null);
            }
            DALProviderManager.DALProvider.NodeDALProvider.DeleteNodePropertyValues(nodeId,
                                                                                    CultureInfo.InvariantCulture.Name,
                                                                                    null);

            //On reload le noeud en bypassant la cache
            node = new Query(new NullCacheProvider(), new QueryDefinition())
                .Where(n => n.ID, Comparison.Equal, nodeId)
                .Execute().FirstOrDefault();

            foreach (var instance in node.Instances)
            {
                instance.HasModifiedProperties = false;
                instance.IsDirty = true;
            }

            foreach (var instance in node.Instances.Where(i => i.Culture.Name != ""))
            {
                if (node.HasName)
                {
                    instance.Name = instance[node.NamePropertyId].GetValue<string>();
                }
                ActionManager.Instance.OnAfterRevertNodeInstance(new ActionEventArgs<NodeInstance>(instance));
            }
            SaveNode(node);
            foreach (var instance in node.Instances.Where(i => i.Culture.Name != ""))
            {
                ActionManager.Instance.OnAfterRevertNodeInstance(new ActionEventArgs<NodeInstance>(instance));
            }
            return (node);
        }

        public static void MoveNode(Node pNode, Guid? pDestinationParentNodeId)
        {
            pNode.ParentID = pDestinationParentNodeId;
            SaveNode(pNode);
            // Le Noeud est déplacé en première position
            // pour ne pas qu'il apparaisse n'importe ou
            // dans la liste
            ChangeNodeOrder(pNode, null, DropPosition.Above);
        }

        /// <summary>
        ///   Change la position d'un noeud
        ///   La nouvelle position du noeud est la position du DestinationNodeId
        ///   Si la Position est Above, le DestinationNode est placé apres
        ///   Si la Direction est Below, le DestinationNode est placé avant
        ///   Si le DestinationNodeId est omis, le Noeud est placé en première position
        /// </summary>
        /// <param name = "pNode"></param>
        /// <param name = "pDestinationNodeId"></param>
        /// <param name = "pPosition"></param>
        /// <returns></returns>
        public static Collection<Node> ChangeNodeOrder(Node pNode,
            Guid? pDestinationNodeId, DropPosition pPosition)
        {
            var siblings = GetSiblingNodes(pNode).ToList();

            if (siblings.Count < 2)
            {
                //Pas d'ordonancement à faire.
                return new Collection<Node>(siblings);
            }

            //Ordonner les Noeuds par position
            siblings.Sort(ComparePosition);

            //Retirer le noeud de la collection
            siblings.RemoveAt(siblings.FindIndex(n => n.ID == pNode.ID));

            //Si destinationNodeId n'a pas de valeur, le noeud ira en première position
            var destinationNodeId = pDestinationNodeId ?? siblings[0].ID;

            //Trouver le noeud situé avant
            var position = 0;

            if (siblings.Exists(n => n.ID == pDestinationNodeId))
            {
                position = siblings.FindIndex(n => n.ID == destinationNodeId) + (int)pPosition;
            }

            //Insertion du Noeud à la bonne position
            siblings.Insert(position, pNode);

            //Mettre à jour la position
            for (short i = 0; i < siblings.Count; i++)
            {
                var currentNode = siblings[i];

                if (currentNode.Position != i)
                {
                    var oldPos = currentNode.Position;
                    currentNode.Position = i;
                    DALProviderManager.DALProvider.NodeDALProvider
                        .SetPosition(currentNode.ID, currentNode.Position);
                    ActionManager.Instance
                        .OnAfterMoveNode(new NodeMovedEventArgs(currentNode, oldPos, i));
                }
            }

            return new Collection<Node>(siblings);
        }

        [Obsolete("Use the overload with DropPosition parameter instead")]
        public static Collection<Node> ChangeNodeOrder(Node pNode,
            Guid? pDestinationNodeId, Direction pDirection)
        {
            var position = pDirection == Direction.Up
                               ? DropPosition.Above
                               : DropPosition.Below;
            return ChangeNodeOrder(pNode, pDestinationNodeId, position);
        }

        /// <summary>
        ///   Retourne la collection de Noeuds ayant le même parent que le Noeud en paramètre
        /// </summary>
        /// <param name = "pNode"></param>
        /// <returns></returns>
        /// <remarks>
        ///   Le Noeud passé en paramètre fait aussi partie de la collection retournée
        /// </remarks>
        public static Collection<Node> GetSiblingNodes(Node pNode)
        {
            var query = NewQuery().Where(n => n.ParentID, Comparison.Equal, pNode.ParentID);

            return new Collection<Node>(query.Execute().ToList());
        }

        public static void DestroyNode(Guid nodeId)
        {
            var node = GetNode(nodeId);

            if (node != null)
                DestroyNode(node);
        }

        public static void DestroyNode(Node pNode)
        {
            DeleteNode(pNode, true);
        }

        public static void DeleteNode(Guid nodeId)
        {
            var node = GetNode(nodeId);

            if (node != null)
                DeleteNode(node);
        }

        public static void DeleteNode(Node pNode)
        {
            DeleteNode(pNode, false);
        }

        private static void DeleteNode(Node pNode, bool pDestroy)
        {
            if (GetChildNodeCount(pNode.ID) > 0)
                throw new Exception("Only leaf nodes can be deleted.");

            //TODO : Ajouter la logique pour pouvoir utiliser SaveType même pour une branche et destroy
            if (pDestroy)
            {
                ActionManager.Instance.OnBeforeDeleteNode(new ActionEventArgs<Node>(pNode));

                DALProviderManager.DALProvider.NodeDALProvider.DeleteNode(pNode.ID, pNode.TypeId, true);

                ActionManager.Instance.OnAfterDeleteNode(new ActionEventArgs<Node>(pNode));
            }
            else
            {
                pNode.Status = EntityStates.Deleted;
                SaveNode(pNode);
            }
        }

        public static void DeleteBranch(Guid nodeId, bool destroy)
        {
            DeleteHierarchy(nodeId, destroy, true, null);
        }

        public static void DeleteHierarchy(Guid nodeId, bool destroy, bool deleteRoot, Guid? typeId)
        {
            var rootNode = NewQuery()
                .Where(n => n.ID, Comparison.Equal, nodeId).Execute().SingleOrDefault();

            if (rootNode == null)
            {
                return;
            }

            var query = NewQuery()
                .Where(n => n.Path, Comparison.Like,
                       rootNode.Path + DataUtil.GetSQLInclusivityOperator(false));

            if (typeId.HasValue)
                query.Where(n => n.NodeTypeID, Comparison.Equal, typeId.Value);

            var childNodes = query.OrderByDesc(n => n.Depth).Execute();

            foreach (var node in childNodes)
            {
                DeleteNode(node, destroy);
            }

            if (deleteRoot)
                DeleteNode(rootNode, destroy);
        }

        public static void DeleteByParent(Guid parendId, bool destroy)
        {
            DeleteHierarchy(parendId, destroy, false, null);
        }

        public static void DeleteBranch(Guid nodeId)
        {
            DeleteBranch(nodeId, false);
        }

        public static bool NodeCanHaveChildren(Guid typeId)
        {
            var nodeType = NodeTypeManager.GetNodeType(typeId);
            if (nodeType == null)
            {
                throw new CoreException(String.Concat("NodeType not found: ", typeId));
            }
            return nodeType.LegitimateChildTypes.Count() > 0;
        }

        public static void SetNodeActiveStatus(Guid nodeId, CultureInfo culture, bool pActive)
        {
            SetNodeActiveStatus(GetNode(nodeId), culture, pActive);
        }

        public static void SetNodeActiveStatus(Node node, CultureInfo culture, bool pActive)
        {
            if (node != null
                && (node.InternalInstances.Exists(culture) || node.IsLanguageNeutral))
            {
                var inst = node.IsLanguageNeutral ? node.DefaultInstance : node.InternalInstances[culture];
                inst.IsActive = pActive;

                ActionManager.Instance.OnBeforeChangeNodeInstanceActiveStatus(new ActionEventArgs<NodeInstance>(inst));
                SaveNode(node);
                ActionManager.Instance.OnAfterChangeNodeInstanceActiveStatus(new ActionEventArgs<NodeInstance>(inst));
            }
        }

        public static List<Node> GetLegitimateParentNodes(Guid pTypeId)
        {
            return DALProviderManager.DALProvider.NodeDALProvider.GetLegitimateParentNodes(pTypeId);
        }

        //public static IEnumerable<Guid> GetNodeIdsByTypeIds(Guid pTypeId)
        //{
        //    throw new NotImplementedException();
        //}

        public static Node NewNode(Guid nodeId, Guid typeId,
            CultureInfo pDefaultInstanceCulture, Guid? pParentNodeId)
        {
            return NewNode(nodeId, typeId, pDefaultInstanceCulture, pParentNodeId, true);
        }

        public static Node NewNode(Guid nodeId, Guid typeId, CultureInfo pDefaultInstanceCulture,
            Guid? pParentNodeId, bool validateHierarchicalRules)
        {
            if (pDefaultInstanceCulture == null)
            {
                throw new ArgumentNullException("pDefaultInstanceCulture");
            }
            if (typeId.Equals(Guid.Empty))
            {
                throw new ArgumentException("typeId cannot be equal to Guid.Empty");
            }

            var node = new Node(nodeId, typeId, pParentNodeId, null, null, false)
                           {
                               Status = EntityStates.New //,
                               //CreatedOn = DateTime.Now --> SubsonicNodeDALProvider
                           };

            if (node.NodeType.IsAbstract)
                throw new Exception("Node creation failed because node type is abstract.");

            //S'assurer que les HierarchicalRules sont respectées
            if (validateHierarchicalRules && !node.RespectsNodeHierarchicalRules())
                throw new Exception(
                    String.Format(
                        "Node creation failed because node hierarchical rules were not respected. Node ID: {0} Type ID: {1}{2}",
                        node.ID, node.NodeTypeID,
                        node.ParentID.HasValue ? " Parent ID: " + node.ParentID.Value : ""));

            if (validateHierarchicalRules && !node.RespectsTypeHierarchicalRules())
                throw new Exception(
                    String.Format(
                        "Node creation failed because type hierarchical rules were not respected. Node ID: {0} Type ID: {1}{2}",
                        node.ID, node.NodeTypeID,
                        node.ParentID.HasValue ? " Parent ID: " + node.ParentID.Value : ""));

            if (pDefaultInstanceCulture.Name != "" || node.NodeType.IsLanguageNeutral)
            {
                node.AddNodeInstance(pDefaultInstanceCulture.Name);
            }
            return node;
        }

        public static T NewNode<T>(Guid nodeId, Guid typeId, CultureInfo pDefaultInstanceCulture,
            Guid? pParentNodeId, bool validateHierarchicalRules)
            where T : INodeWrapper, new()
        {
            return new T
                       {
                           InnerNode =
                               NewNode(nodeId, typeId, pDefaultInstanceCulture, pParentNodeId,
                                       validateHierarchicalRules)
                       };
        }

        public static T NewNode<T>(Guid nodeId, Guid typeId, CultureInfo pDefaultInstanceCulture,
            Guid? pParentNodeId)
            where T : INodeWrapper, new()
        {
            return new T { InnerNode = NewNode(nodeId, typeId, pDefaultInstanceCulture, pParentNodeId) };
        }

        public static T NewNode<T>(Guid typeId, CultureInfo pDefaultInstanceCulture, Guid? pParentNodeId)
            where T : INodeWrapper, new()
        {
            return new T { InnerNode = NewNode(Guid.NewGuid(), typeId, pDefaultInstanceCulture, pParentNodeId) };
        }

        public static T NewNode<T>(CultureInfo pDefaultInstanceCulture)
            where T : INodeWrapper, ISystemNode, new()
        {
            return NewNode<T>(pDefaultInstanceCulture, null);
        }

        public static T NewNode<T>(CultureInfo pDefaultInstanceCulture, Guid? pParentNodeID)
            where T : INodeWrapper, ISystemNode, new()
        {
            return NewNode<T>(new T().SystemNodeTypeID, pDefaultInstanceCulture, pParentNodeID);
        }

        public static Node NewNode(Guid typeId, CultureInfo pDefaultInstanceCulture)
        {
            return NewNode(typeId, pDefaultInstanceCulture, null);
        }

        public static Node NewNode(Guid typeId, CultureInfo pDefaultInstanceCulture,
            Guid? pParentNodeId)
        {
            return NewNode(Guid.NewGuid(), typeId, pDefaultInstanceCulture, pParentNodeId);
        }

        //
        //  Approbation
        //
        public static T ApproveNode<T>(T node, CultureInfo culture)
            where T : INodeWrapper
        {
            bool approbationEnabled = SecurityManager.ApprobationEnabled;
            var inst = node.InnerNode.Instances.FirstOrDefault(ni => ni.Culture.Name.Equals(culture.Name, StringComparison.InvariantCultureIgnoreCase));

            if (approbationEnabled)
            {
                if (inst != null)
                {
                    ActionManager.Instance.OnBeforeApproveNodeInstance(new ActionEventArgs<NodeInstance>(inst));
                    inst.Approve();
                }
            }

            SaveNode(node.InnerNode);

            if (approbationEnabled)
            {
                if (inst != null)
                {
                    ActionManager.Instance.OnAfterApproveNodeInstance(new ActionEventArgs<NodeInstance>(inst));
                }
            }

            return node;
        }

        public static T SaveAndApproveNode<T>(T node, CultureInfo culture)
            where T : INodeWrapper
        {
            bool approbationEnabled = SecurityManager.ApprobationEnabled;
            var inst = node.InnerNode.Instances.FirstOrDefault(ni => ni.Culture.Name.Equals(culture.Name, StringComparison.InvariantCultureIgnoreCase));

            if (approbationEnabled && inst != null)
            {
                //ActionManager.Instance.OnBeforeApproveNodeInstance(new ActionEventArgs<NodeInstance>(inst));
                inst.SetApprobationLevel(false);
            }

            SaveNode(node.InnerNode);

            //if (approbationEnabled && inst != null)
            //{
            //    ActionManager.Instance.OnAfterApproveNodeInstance(new ActionEventArgs<NodeInstance>(inst));
            //}

            return node;
        }

        public static Node SaveApproveAndPublishNode(Node node, CultureInfo culture)
        {
            var approbationEnabled = SecurityManager.ApprobationEnabled;
            var inst = node.Instances.FirstOrDefault(ni => ni.Culture.Name.Equals(culture.Name, StringComparison.InvariantCultureIgnoreCase));

            if (approbationEnabled && inst != null)
            {
                //ActionManager.Instance.OnBeforeApproveNodeInstance(new ActionEventArgs<NodeInstance>(inst));
                inst.ResetApprobationLevel();
            }

            //var savedNode = SaveNode(node.InnerNode);
            PublishNodes(new[] { node }, culture);

            //if (approbationEnabled && inst != null)
            //{
            //    ActionManager.Instance.OnAfterApproveNodeInstance(new ActionEventArgs<NodeInstance>(inst));
            //}

            return node;
        }

        public static T RevertAndApproveNode<T>(T node, CultureInfo culture)
            where T : INodeWrapper
        {
            bool approbationEnabled = SecurityManager.ApprobationEnabled;
            var inst = node.InnerNode.Instances.FirstOrDefault(ni => ni.Culture.Name.Equals(culture.Name, StringComparison.InvariantCultureIgnoreCase));

            if (approbationEnabled && inst != null)
            {
                //ActionManager.Instance.OnBeforeApproveNodeInstance(new ActionEventArgs<NodeInstance>(inst));
                inst.ResetApprobationLevel();
            }

            RevertNode(node.InnerNode.ID, culture);

            //if (approbationEnabled && inst != null)
            //{
            //    ActionManager.Instance.OnAfterApproveNodeInstance(new ActionEventArgs<NodeInstance>(inst));
            //}

            return node;
        }

        public static Node SaveNode(Node pNode)
        {
            SaveNodes(new[] { pNode });
            return pNode;
        }

        public static T SaveNode<T>(T pNode)
            where T : INodeWrapper
        {
            SaveNodes(new[] { pNode.InnerNode });
            return pNode;
        }

        public static IEnumerable<Node> SaveNodes(IEnumerable<Node> nodes)
        {
            return SaveNodes(nodes, true);
        }

        public static IEnumerable<Node> SaveNodes(IEnumerable<Node> nodes, bool validateHierarchicalRules)
        {
            var nodesToSave = nodes.ToList();

            lock (Locker)
            {
                var nodeDataShells = new List<NodeDataShell>();

                foreach (var node in nodesToSave)
                {
                    List<ManyToManyNodeProperty> manyToManyProperties;

                    switch (node.Status)
                    {
                        case EntityStates.Deleted:
                            if (GetChildNodeCount(node.ID) > 0)
                                throw new Exception("Only leaf nodes can be deleted.");

                            manyToManyProperties = node.DefaultInstance.Properties.Where(p => !p.IsOwnProperty).OfType
                                    <ManyToManyNodeProperty>().Distinct().ToList();
                            manyToManyProperties.SelectMany(p => p.GetCurrentRelationsValues())
                                .ForEach(n => ActionManager.Instance
                                    .OnBeforeDeleteNode(new ActionEventArgs<Node>(n.InnerNode)));

                            ActionManager.Instance.OnBeforeDeleteNode(new ActionEventArgs<Node>(node));

                            //Destroy et/ou delete branch pas possible pour l'instant
                            nodeDataShells.Add(new NodeDataShell(node));
                            break;
                        case EntityStates.DeletedBranch:
                            DeleteBranch(node.ID);
                            break;
                        case EntityStates.Loaded:
                            nodeDataShells.Add(new NodeDataShell(node));
                            break;
                        default:
                            var manyToManyNodes = new List<ManyToManyRelationNode>();
                            var deletedManyToManyNodes = new List<ManyToManyRelationNode>();
                            var instances = new List<NodeInstance>();
                            var deletedInstances = new List<NodeInstance>();
                            var properties = new List<NodeProperty>();
                            var oldValues = new Dictionary<NodeProperty, object>();

                            var currentNode = node;
                            var parentNode = nodesToSave
                                .Where(n => n.ID == currentNode.ParentID).FirstOrDefault();

                            if (validateHierarchicalRules && !node.RespectsNodeHierarchicalRules())
                                throw new Exception(String.Format("Node save failed because node hierarchical rules were not respected. Node ID: {0} Type ID: {1}{2}",
                                    node.ID, node.NodeTypeID,
                                    node.ParentID.HasValue ? " Parent ID: " + node.ParentID.Value : ""));

                            if ((parentNode != null && (validateHierarchicalRules &&
                                !node.RespectsTypeHierarchicalRules(parentNode))) &&
                                !node.RespectsTypeHierarchicalRules())
                                throw new Exception(String.Format("Node save failed because type hierarchical rules were not respected. Node ID: {0} Type ID: {1}{2}",
                                    node.ID, node.NodeTypeID,
                                    node.ParentID.HasValue ? " Parent ID: " + node.ParentID.Value : ""));

                            if (node.Instances.Count == 0)
                                throw new Exception("Nodes must contain at least one instance.");

                            if (node.InstancesLoaded)
                            {
                                instances.AddRange(node.InternalInstances);
                                deletedInstances.AddRange(node.InternalInstances.Removed);
                            }

                            //Sélection de toutes les propriétés d'un noeud qui sont présentement chargées
                            properties.AddRange(instances.Where(i => i.PropertiesLoaded)
                                .SelectMany(inst => inst.Properties.Where(p => p.IsOwnProperty))
                                .Distinct());

                            /*Many To Many*/
                            manyToManyProperties = instances.Where(i => i.PropertiesLoaded).SelectMany(
                                    inst => inst.Properties.Where(p => !p.IsOwnProperty))
                                    .OfType<ManyToManyNodeProperty>().Distinct().Where(m => m.ForeignProperty.PrimaryMedianamikControlID.HasValue).ToList();

                            //manyToManyProperties[0].Relation.LeftRelation.

                            manyToManyNodes.AddRange(manyToManyProperties.SelectMany(p => p.GetCurrentRelationsValues()));
                            deletedManyToManyNodes.AddRange(manyToManyProperties.SelectMany(p => p.GetRemovedRelationsValues()));
                            var manyToManyPropertyInstances = manyToManyNodes.Select(n => n.DefaultInstance
                                /*Les ManyToMany sont Invariant à la Culture*/).ToList();
                            properties.AddRange(manyToManyPropertyInstances
                                .SelectMany(inst => inst.Properties.Where(p => p.IsOwnProperty)));

                            instances.AddRange(manyToManyPropertyInstances);

                            //Marquer les Property qui ont une révision comme NEW
                            //On ne doit jamais écraser une révision précédente
                            properties.ForEach(p => p.OnBeforeSave());

                            var allNodes = manyToManyNodes.Select(n => n.InnerNode).Union(new List<Node> { node });

                            if (node.Status == EntityStates.New)
                            {
                                allNodes.ForEach(
                                    n => ActionManager.Instance.OnBeforeCreateNode(new ActionEventArgs<Node>(n)));
                                instances.ForEach(
                                    inst =>
                                    ActionManager.Instance.OnBeforeCreateNodeInstance(
                                        new ActionEventArgs<NodeInstance>(inst)));
                            }
                            else
                            {
                                allNodes.Where(n => n.Status != EntityStates.New).ForEach(n =>
                                    ActionManager.Instance.OnBeforeUpdateNode(new ActionEventArgs<Node>(n)));
                                allNodes.Where(n => n.Status == EntityStates.New).ForEach(n =>
                                    ActionManager.Instance.OnBeforeCreateNode(new ActionEventArgs<Node>(n)));
                                instances.Where(i => !i.IsNew).ForEach(inst =>
                                    ActionManager.Instance.OnBeforeUpdateNodeInstance(
                                        new ActionEventArgs<NodeInstance>(inst)));
                                instances.Where(i => i.IsNew).ForEach(inst =>
                                    ActionManager.Instance.OnBeforeCreateNodeInstance(
                                        new ActionEventArgs<NodeInstance>(inst)));
                                deletedInstances.ForEach(inst =>
                                    ActionManager.Instance.OnBeforeDeleteNodeInstance(
                                        new ActionEventArgs<NodeInstance>(inst)));
                                deletedManyToManyNodes.ForEach(n =>
                                    ActionManager.Instance.OnBeforeDeleteNode(new ActionEventArgs<Node>(n.InnerNode)));
                            }

                            instances.Where(i => i.ToBePublished).ForEach(i => ActionManager.Instance
                                .OnBeforePublishNodeInstance(new ActionEventArgs<NodeInstance>(i)));

                            properties.Where(p => p.IsDirty).ForEach(p => oldValues.Add(p, p.Value));

                            nodeDataShells.Add(new NodeDataShell(node, properties,
                                 instances, deletedInstances, manyToManyNodes,
                                 deletedManyToManyNodes, oldValues));

                            break;
                    }
                }

                DALProviderManager.DALProvider.NodeDALProvider.TransacSave(nodeDataShells);

                foreach (var nodeDataShell in nodeDataShells.Where(n => n.Node.Status != EntityStates.Loaded))
                {
                    var fixedNodeDataShell = nodeDataShell;

                    //TODO : Handle DeletedBranch
                    if (nodeDataShell.Node.Status == EntityStates.Deleted)
                    {
                        ActionManager.Instance.OnAfterDeleteNode(new ActionEventArgs<Node>(nodeDataShell.Node));
                        //TODO : ???
                        nodesToSave.RemoveAt(nodesToSave.FindIndex(n => n.ID.Equals(fixedNodeDataShell.Node.ID)));
                    }
                    else
                    {
                        var allNodes = nodeDataShell.ManyToManyNodes
                            .Select(n => n.InnerNode).Union(new List<Node> { nodeDataShell.Node });

                        if (nodeDataShell.Node.Status == EntityStates.New)
                        {
                            nodesToSave.First(n => n.ID.Equals(fixedNodeDataShell.Node.ID)).MarkSaved();
                            allNodes.ForEach(n => ActionManager.Instance.OnAfterCreateNode(new ActionEventArgs<Node>(n)));
                            nodeDataShell.Instances.ForEach(inst => ActionManager.Instance
                                .OnAfterCreateNodeInstance(new ActionEventArgs<NodeInstance>(inst)));
                            nodeDataShell.Properties.ForEach(p => ActionManager.Instance
                                .OnAfterCreateNodeProperty(new NodePropertyActionEventArgs(p, null, p.Value)));
                        }
                        else
                        {
                            var updatedNodes = new List<Node>(allNodes.Where(n => n.Status != EntityStates.New));
                            var dirtyNodes = new List<Node>(updatedNodes.Where(n => n.Status == EntityStates.Dirty));
                            var newNodes = new List<Node>(allNodes.Where(n => n.Status == EntityStates.New));
                            var newInstances = new List<NodeInstance>(nodeDataShell.Instances.Where(i => i.IsNew));
                            var updatedInstances = new List<NodeInstance>(nodeDataShell.Instances.Where(i => !i.IsNew));
                            var newProperties = new List<NodeProperty>(nodeDataShell.Properties.Where(p => p.IsNew));
                            var dirtyProperties = new List<NodeProperty>(nodeDataShell.Properties.Where(p => p.IsDirty));

                            nodesToSave.First(n => n.ID.Equals(fixedNodeDataShell.Node.ID)).MarkSaved();

                            updatedNodes.ForEach(n => ActionManager.Instance
                                .OnAfterUpdateNode(new ActionEventArgs<Node>(n)));
                            newNodes.ForEach(n => ActionManager.Instance
                                .OnAfterCreateNode(new ActionEventArgs<Node>(n)));
                            updatedInstances.ForEach(inst => ActionManager.Instance
                                .OnAfterUpdateNodeInstance(new ActionEventArgs<NodeInstance>(inst)));
                            newInstances.ForEach(inst => ActionManager.Instance
                                .OnAfterCreateNodeInstance(new ActionEventArgs<NodeInstance>(inst)));
                            nodeDataShell.DeletedInstances.ForEach(inst => ActionManager.Instance
                                .OnAfterDeleteNodeInstance(new ActionEventArgs<NodeInstance>(inst)));
                            dirtyProperties.ForEach(p => ActionManager.Instance
                                .OnAfterUpdateNodeProperty(new NodePropertyActionEventArgs(p,
                                    fixedNodeDataShell.OldValues[p], p.Value)));
                            newProperties.ForEach(p => ActionManager.Instance
                                .OnAfterCreateNodeProperty(new NodePropertyActionEventArgs(p, null, p.Value)));
                        }

                        nodeDataShell.Instances.Where(i => i.ToBePublished).ForEach(i =>
                        {
                            ActionManager.Instance.OnAfterPublishNodeInstance(new ActionEventArgs<NodeInstance>(i));
                            i.ToBePublished = false;
                        });
                    }
                }

                foreach (var id in nodeDataShells
                    .Where(n => n.Node.Status == EntityStates.Loaded)
                    .Select(nodeDataShell => nodeDataShell.Node.ID)
                    .Where(LockManager.IsLocked))
                {
                    LockManager.UnlockObject(id);
                }

                return nodesToSave;
            }
        }

        public static int GetChildNodeCount(Guid nodeId)
        {
            var dico = DALProviderManager.DALProvider.NodeDALProvider.ChildNodeCount(new[] { nodeId });
            if (dico.ContainsKey(nodeId))
            {
                return dico[nodeId];
            }
            return 0;
        }

        public static Dictionary<Guid, int> GetChildNodeCount(Guid[] nodeIDs)
        {
            return DALProviderManager.DALProvider.NodeDALProvider.ChildNodeCount(nodeIDs);
        }

        public static List<Node> GetChildNodes(Guid nodeId, bool inclusive, int depth)
        {
            return GetChildNodes(nodeId, default(Guid?), inclusive, depth).ToList();
        }

        public static List<Node> GetChildNodes(Guid nodeId)
        {
            return GetChildNodes(nodeId, default(Guid?), false, 1).ToList();
        }

        public static List<T> GetChildNodes<T>(Guid nodeId, bool inclusive, int depth)
            where T : INodeWrapper, ISystemNode, new()
        {
            return GetChildNodes(nodeId, new T().SystemNodeTypeID, inclusive, depth)
                .Select(n => new T { InnerNode = n })
                .ToList();
        }

        public static List<T> GetChildNodes<T>(Guid nodeId)
            where T : INodeWrapper, ISystemNode, new()
        {
            return GetChildNodes(nodeId, null, false, 1)
                .Select(n => new T { InnerNode = n })
                .ToList();
        }

        private static IEnumerable<Node> GetChildNodes(Guid nodeId, Guid? nodeTypeId,
            bool inclusive, int depth)
        {
            var rootNode = GetNode(nodeId);

            if (rootNode == null)
                yield break;

            var query = NewQuery();

            if (depth == 1)
            {
                //Enfants directs
                query.Where(n => n.ParentID, Comparison.Equal, nodeId);
            }
            else if (depth == 0)
            {
                //Tous les descendants
                //Le critère sur le path est suffisant mais le critère sur le Depth permet d'accélérer un peu la requète
                query.Where(n => n.Path, Comparison.Like, rootNode.Path + DataUtil.GetSQLInclusivityOperator(false))
                    .Where(n => n.Depth, Comparison.MoreThan, rootNode.Depth);
            }
            else
            {
                //Limiter a un certain niveau de profondeur
                query.Where(n => n.Path, Comparison.Like, rootNode.Path + DataUtil.GetSQLInclusivityOperator(false))
                    .Where(n => n.Depth, Comparison.LessThanOrEqual, rootNode.Depth + depth)
                    .Where(n => n.Depth, Comparison.MoreThan, rootNode.Depth);
            }

            if (nodeTypeId.HasValue)
            {
                var type = NodeTypeManager.GetNodeType(nodeTypeId.Value);
                if (type == null)
                {
                    throw new ArgumentException("NodeType does not exists:" + nodeTypeId.Value);
                }
                var types = NodeTypeManager.GetAllChildTypes(nodeTypeId.Value).Concat(new[] { type });
                query.Where(n => n.NodeTypeID, Comparison.In, types.Select(t => t.ID).ToArray());
            }

            if (inclusive)
            {
                yield return rootNode;
            }

            foreach (var node in query.Execute())
            {
                yield return node;
            }
        }

        /// <summary>
        ///   Retourne une liste de parents
        /// </summary>
        /// <remarks>
        ///   Cette liste n'est pas ordonnée !
        /// </remarks>
        /// <typeparam name = "T"></typeparam>
        /// <param name = "pNode"></param>
        /// <param name = "nodeTypeId"></param>
        /// <param name = "inclusive"></param>
        /// <param name = "depth"></param>
        /// <returns></returns>
        public static List<T> GetParentNodes<T>(INode pNode, Guid? nodeTypeId,
            bool inclusive, int depth)
            where T : INodeWrapper, new()
        {
            return GetParentNodes(pNode, nodeTypeId, inclusive, depth)
                .Select(n => new T { InnerNode = n })
                .ToList();
        }

        public static List<Node> GetParentNodes(INode pNode, Guid? nodeTypeId,
            bool inclusive, int depth)
        {
            var query = NewQuery().Where(n => n.ID, Comparison.In,
                DataUtil.GetBottomToTopNodeIds(pNode.Path, inclusive, depth).ToArray());
            var parentNodes = query.Execute();

            if (nodeTypeId.HasValue)
            {
                parentNodes = parentNodes.Where(parentNode => parentNode.NodeType.GetParentIds(true)
                    .Any(parentTypeID => parentTypeID == nodeTypeId.Value)).ToList();
            }

            return parentNodes.ToList();
        }

        internal static IEnumerable<ManyToManyRelationNode> GetManyToManyValues(ManyToManyNodeProperty pNodeProperty)
        {
            var leftPrimaryNodeId = pNodeProperty.Node.ID;
            var leftForeignPropertyId = pNodeProperty.Relation.LeftRelation.ForeignPropertyID;

            var query = MakeGetNodeByTypeQuery(pNodeProperty.Relation.LeftRelation.ForeignTypeID, true)
                .Where(leftForeignPropertyId, Comparison.Equal, leftPrimaryNodeId);

            return query.AsQuery<ManyToManyRelationNode>().Execute();
        }

        public static DateTime[] GetNodeRevisions(Guid nodeId, CultureInfo culture)
        {
            return DALProviderManager.DALProvider.NodeDALProvider.GetNodeRevisions(nodeId, culture.Name);
        }

        public static IEnumerable<Publication> GetNodePublications(Guid nodeId, CultureInfo culture)
        {
            return DALProviderManager.DALProvider.NodeDALProvider.GetNodePublications(nodeId, culture.Name);
        }

        public static List<Publication> GetNewNodePublications(Guid nodeId, CultureInfo culture)
        {
            return DALProviderManager.DALProvider.NodeDALProvider.GetNewNodePublications(nodeId, culture.Name);
        }

        public static bool PropertyValueIsUnique(Guid nodeID, Guid nodeTypeId, Guid propertyId, object value)
        {
            var strValue = (value == null) ? string.Empty : value.ToString();

            var query = MakeGetNodeByTypeQuery(nodeTypeId, false).Where(n => n.ID, Comparison.NotEqual, nodeID).Where(propertyId, Comparison.Equal, strValue);

            return !query.Execute().Any();
        }

        //Méthode pour générer un URL
        public static string GetCurrentPath(Guid nodeId, string cultureName, string pageName)
        {
            //TODO : Effectuer une certaine logique en fonction du pageName

            return DALProviderManager.DALProvider.NodeDALProvider.GetCurrentPath(nodeId, MedianamikProperties.Name,
                                                                                 cultureName);
        }

        internal static RelationOptionCollection GetRelationOptions(OneToManyRelation relation,
            string cultureName, Node node, Guid? rootNodeId)
        {
            var options = DALProviderManager.DALProvider.NodeDALProvider
                .GetRelationOptions(relation, rootNodeId)
                .Distinct(new RelationOptionEqualityComparer()).ToList();

            var coll = new RelationOptionCollection();
            options.ForEach(coll.Add);

            return coll;
        }

        private static Query NewQuery()
        {
            return NewQuery(false);
        }

        private static Query NewQuery(bool withDeletedNodes)
        {
            return CacheProvider == null
                       ? new Query(withDeletedNodes)
                       : new Query(CacheProvider, null, withDeletedNodes);
        }

        private static int ComparePosition(Node n1, Node n2)
        {
            return n1.LongPosition.CompareTo(n2.LongPosition);
        }

        [Obsolete("Use the overload without startDate and endDate parameters instead")]
        public static void PublishNode(Guid nodeId, CultureInfo culture,
            DateTime startDate, DateTime? endDate)
        {
            PublishNode(nodeId, culture);
        }

        [Obsolete("Use the overload without startDate and endDate parameters instead")]
        public static void PublishNodes(IEnumerable<Guid> publishNodeIds, CultureInfo culture,
            DateTime startDate, DateTime? endDate)
        {
            PublishNodes(publishNodeIds, culture);
        }
    }
}